﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Threading;
using System.Collections.Specialized;
using System.IO;
using System.Threading.Tasks;
using System.Xml;
using System.Diagnostics.Contracts;
using ShredTheLog.Common;
using System.Windows.Threading;
using ShredTheLog.Api.DataContracts;
using ShredTheLog.Api.DataContracts.TextChat;
using ShredTheLog.Api;
using System.Collections.Concurrent;

namespace ShredTheLog.Model
{
    internal sealed partial class TextChatModel : ITextChat
    {
        private int _uid;

        public Guid GUID { get; private set; }

        private ShredClient _shredClient;

        private int _updatesCount;

        private int _lastInUpdateID;

        [ContractInvariantMethod]
        private void ObjectInvariant()
        {
            Contract.Invariant(_rooms != null);
            Contract.Invariant(_uid > 0);
            Contract.Invariant(_lastInUpdateID >= 0);
            Contract.Invariant(_updatesCount >= 1);
        }

        private DispatcherTimer _updateTimer;

        internal TextChatModel(ISessionCredentials credentials, IInitialTextChatInfo initialInfo, ShredClient shredClient)
        {
            Contract.Requires(initialInfo != null);
            Contract.Requires(initialInfo.Uid > 0);
            Contract.Requires(shredClient != null);

            WhenUserEnters = _whenUserEntersSubject.AsObservable();
            WhenUserLeaves = _whenUserLeavesSubject.AsObservable();
            WhenRoomOpened = _whenRoomOpenedSubject.AsObservable();
            WhenUserEntersRoom = _whenUserEntersRoomSubject.AsObservable();
            WhenUserLeavesRoom = _whenUserLeavesRoomSubject.AsObservable();
            WhenMessageReceived = _whenMessageReceivedSubject.AsObservable();
            WhenUserTypes = _whenUserTypesSubject.AsObservable();
            WhenSysMessageReceived = _whenSysMessageReceivedSubject.AsObservable();
            WhenPrivateRequestAnswerReceived = _whenPrivateRequestAnswerReceivedSubject.AsObservable();
            WhenPrivateRequestReceived = _whenPrivateRequestReceivedSubject.AsObservable();
            WhenErrorOccurred = _whenErrorOccurredSubject.AsObservable();

            _shredClient = shredClient;
            _updatesCount = 1;
            _lastInUpdateID = 0;
            _uid = initialInfo.Uid;
            Credentials = credentials;
            GUID = initialInfo.Guid;
            _updateTimer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1), IsEnabled = false };
            _updateTimer.Tick += (sender, e) => Update();
        }

        public bool OpenRoom(string roomName)
        {
            if (_rooms.TryAdd(roomName, RoomState.Opening))
            {
                Update(new EnterRoomCommand() { RoomName = roomName });
                return true;
            }
            else
            {
                return false;
            }
        }

        private DateTime _lastUpdateTime;

        public bool IsConnected { get { return _updateTimer.IsEnabled; } set { _updateTimer.IsEnabled = value; } }

        private List<TextChatUpdate> _receivedUpdates = new List<TextChatUpdate>();
        private void ProcessReceivedUpdates(IEnumerable<TextChatUpdate> newUpdates)
        {
            Contract.Requires(newUpdates != null);
            Contract.Requires(Contract.ForAll(newUpdates, u => u != null));

            // Полученные от сервера обновления нужно обрабатывать строго в нужном порядке! 
            // Если порядок нарушен, то пользователь может "уйти" из чата раньше, чем "пришел" 
            // (что в свою очередь наверняка вызовет исключение на клиенте).
            // Складываем обновления в очередь и выбираем их оттуда на обработку строго в порядке увеличения Id, не пропуская ни одного обновления!
            _receivedUpdates.AddRange(newUpdates);
            TextChatUpdate update;
            while ((update = _receivedUpdates.SingleOrDefault(u => u.Id == _lastInUpdateID + 1)) != null)
            {
                ExecuteAction(update.UpdateAction);
                _lastInUpdateID = update.Id; Contract.Assume(_lastInUpdateID > 0);
            }
        }

        ConcurrentDictionary<string, RoomState> _rooms = new ConcurrentDictionary<string, RoomState>();

        private Queue<ITextChatCommand> _commandQueue = new Queue<ITextChatCommand>();
        private int _updateCount = 0;
        public void Update(params ITextChatCommand[] commands)
        {
            if (!IsConnected) return;

            if (_updateCount == 0)
            {
                if ((DateTime.Now - _lastUpdateTime) > _shredClient.UpdatePeriod || commands.Any())
                {
                    _updateCount++;
                    _lastUpdateTime = DateTime.Now;
                    Action<Exception> onError =
                        ex =>
                        {
                            IsConnected = false;
                            _whenErrorOccurredSubject.OnNext(ex);
                        };
                    // Отберем только уже открытые комнаты
                    // и отсеем комнаты, команда на открытие которых уже была послана, 
                    // но подтверждение открытия от сервера еще не пришло.
                    string[] rooms = (from i in _rooms.ToArray() where i.Value == RoomState.Opened select i.Key).ToArray();

                    Contract.Assume(rooms != null);
                    _shredClient.GetTextChatUpdates(rooms, _updatesCount, _lastInUpdateID, GUID, _uid, commands)
                        .Subscribe(
                        result =>
                        {
                            try
                            {
                                if (result.Items != null && result.Items.Length != 0)
                                    ProcessReceivedUpdates(result.Items);

                                if (result.LastUpdate >= _updatesCount)
                                    _updatesCount = result.LastUpdate + 1;

                                _updateCount--;
                                if (_commandQueue.Any())
                                    Update(_commandQueue.DequeueAll());
                            }
                            catch (Exception ex)
                            {
                                onError(ex);
                            }
                        },
                        onError);
                }
            }
            else
            {
                commands.ForEach(c => _commandQueue.Enqueue(c));
            }
        }

        private void ExecuteAction(ITextChatUpdate action)
        {
            Contract.Requires(action != null);

            if (action is AddMessageUpdate)
            {
                var a = (AddMessageUpdate)action;
                if (_rooms.ContainsKey(a.RoomName))
                    _whenMessageReceivedSubject.OnNext(a);
            }
            else if (action is AddUserUpdate)
            {
                var a = (AddUserUpdate)action;
                if (String.IsNullOrEmpty(a.RoomName))
                {
                    _whenUserEntersSubject.OnNext(a);
                }
                else
                {
                    if (_rooms.ContainsKey(a.RoomName))
                        _whenUserEntersRoomSubject.OnNext(a);
                }
            }
            else if (action is DropUserUpdate)
            {
                var a = (DropUserUpdate)action;
                if (String.IsNullOrEmpty(a.RoomName))
                {
                    _whenUserLeavesSubject.OnNext(a);
                }
                else
                {
                    if (_rooms.ContainsKey(a.RoomName))
                        _whenUserLeavesRoomSubject.OnNext(a);
                }
            }
            else if (action is EnterRoomUpdate)
            {
                var a = (EnterRoomUpdate)action;
                _rooms.TryUpdate(a.RoomName, RoomState.Opened, RoomState.Opening);
                _whenRoomOpenedSubject.OnNext(a);
            }
            else if (action is FlashPenUpdate)
            {
                var a = (FlashPenUpdate)action;
                if (_rooms.ContainsKey(a.RoomName))
                    _whenUserTypesSubject.OnNext(a);
            }
            else if (action is SysMessageUpdate)
            {
                var a = (SysMessageUpdate)action;

                if (a.Answer.HasValue)
                    _whenPrivateRequestAnswerReceivedSubject.OnNext(a);
                else
                    _whenSysMessageReceivedSubject.OnNext(a);
            }
            else if (action is PrivateChatRequestUpdate)
            {
                var a = (PrivateChatRequestUpdate)action;
                _whenPrivateRequestReceivedSubject.OnNext(a);
            }
            else
                throw new NotSupportedException(String.Format("Action «{0}» is not supported.", action.ToString()));
        }

        private Subject<IShortUserInfo> _whenUserEntersSubject = new Subject<IShortUserInfo>();
        public IObservable<IShortUserInfo> WhenUserEnters { get; private set; }

        private Subject<IUserInfo> _whenUserLeavesSubject = new Subject<IUserInfo>();
        public IObservable<IUserInfo> WhenUserLeaves { get; private set; }

        private Subject<IEnterRoomUpdate> _whenRoomOpenedSubject = new Subject<IEnterRoomUpdate>();
        public IObservable<IEnterRoomUpdate> WhenRoomOpened { get; private set; }

        private Subject<IRoomUserUpdate> _whenUserEntersRoomSubject = new Subject<IRoomUserUpdate>();
        public IObservable<IRoomUserUpdate> WhenUserEntersRoom { get; private set; }

        private Subject<IRoomUserUpdate> _whenUserLeavesRoomSubject = new Subject<IRoomUserUpdate>();
        public IObservable<IRoomUserUpdate> WhenUserLeavesRoom { get; private set; }

        private Subject<IAddMessageUpdate> _whenMessageReceivedSubject = new Subject<IAddMessageUpdate>();
        public IObservable<IAddMessageUpdate> WhenMessageReceived { get; private set; }

        private Subject<IRoomUserUpdate> _whenUserTypesSubject = new Subject<IRoomUserUpdate>();
        public IObservable<IRoomUserUpdate> WhenUserTypes { get; private set; }

        private Subject<SysMessage> _whenSysMessageReceivedSubject = new Subject<SysMessage>();
        public IObservable<SysMessage> WhenSysMessageReceived { get; private set; }

        private Subject<SysMessage> _whenPrivateRequestAnswerReceivedSubject = new Subject<SysMessage>();
        public IObservable<SysMessage> WhenPrivateRequestAnswerReceived { get; private set; }

        private Subject<IUserInfo> _whenPrivateRequestReceivedSubject = new Subject<IUserInfo>();
        public IObservable<IUserInfo> WhenPrivateRequestReceived { get; private set; }

        private Subject<Exception> _whenErrorOccurredSubject = new Subject<Exception>();
        public IObservable<Exception> WhenErrorOccurred { get; private set; }

        public ISessionCredentials Credentials { get; private set; }

        public void SendMessage(string roomName, string messageText)
        {
            Update(new SendMessageCommand() { RoomName = roomName, MessageText = messageText });
            _whenMessageReceivedSubject.OnNext(new AddMessageUpdate() { Uid = Credentials.Uid, RoomName = roomName, Text = messageText });
        }

        public void CloseRoom(string roomName)
        {
            RoomState roomState;
            _rooms.TryRemove(roomName, out roomState);
        }

        public void Dispose()
        {
            IsConnected = false;
        }
    }

    partial class TextChatModel
    {
        enum RoomState
        {
            Opening,
            Opened
        }
    }
}

